package game.games.rpg;

import game.gfx.ColoredChar;
import game.gfx.Colors;
import game.gfx.ConsoleColor;
import game.unicode.BlockElements;
import java.awt.Dimension;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class Room extends AttributedObject
{
    public static final char    WALL          = '#';
    public static final char    SUPERWALL     = '%';
    
    public static final char    DOOR          = '+';
    public static final char    STAIRS        = '>';
    
    public static final char    WATER         = 'W';
    public static final char    LAVA          = 'L';
    
    public static final char    EMPTY         = ' ';
    public static final char    FLOOR         = '.';
    public static final char    CORRIDOR      = '-';
    
    public static final char    ACTION        = '!';
    
    public static final char    GOLD          = '$';
    public static final char    LOOT          = '?';
    
    public static final char    MONSTER       = 'M';
    public static final char    PLAYER        = 'P';
    
    private Dimension           size;
    private ColoredChar[][]     room;
    
    private Player              player;
    private Point               playerPosition;
    
    private boolean             clearExplored = true;
    private char[]              neverClear    = new char[] { WALL, SUPERWALL, CORRIDOR, DOOR, WATER, LAVA };
    private Map<Point, Boolean> explored;
    
    private int                 viewRadius    = 3;
    private boolean             seeEverything = false;
    private List<Point>         visible;
    
    private Map<Point, Monster> monsters;
    private Map<Point, Door>    doors;
    
    public Room(int width, int height)
    {
        init(width, height);
        // we can explore
        explored = Collections.synchronizedMap(new HashMap<Point, Boolean>());
        visible = Collections.synchronizedList(new ArrayList<Point>());
        // monsters
        monsters = new HashMap<Point, Monster>();
        // doors can be open or closed
        doors = new HashMap<Point, Door>();
    }
    
    public Room()
    {
        this(0, 0);
    }
    
    public void init(int width, int height)
    {
        this.size = new Dimension(width, height);
        this.room = new ColoredChar[height][width];
        // create room
        if (width > 0 && height > 0)
        {
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    room[y][x] = new ColoredChar(EMPTY, ConsoleColor.DEFAULT);
                }
            }
        }
    }
    
    public void init(Dimension d)
    {
        init(d.width, d.height);
    }
    
    public void setChars(String chars)
    {
        if (chars.length() != (size.width * size.height))
        {
            throw new IllegalArgumentException("wrong length: " + chars.length() + "; size: " + size + " => " + (size.width * size.height));
        }
        int index = 0;
        for (int y = 0; y < size.height; y++)
        {
            for (int x = 0; x < size.width; x++)
            {
                char c = chars.charAt(index++);
                // ignore monsers and players because this method is only used to load a savegame which already has monster and player info
                if (PLAYER == c || MONSTER == c)
                {
                    c = FLOOR;
                }
                room[y][x].setChar(c);
            }
        }
    }
    
    public Dimension getSize()
    {
        return size;
    }
    
    public int getWidth()
    {
        return size.width;
    }
    
    public int getHeight()
    {
        return size.height;
    }
    
    private void checkXY(int x, int y)
    {
        if (x < 0)
        {
            throw new IllegalArgumentException("x:" + x + " < 0");
        }
        if (y < 0)
        {
            throw new IllegalArgumentException("y: " + y + " < 0");
        }
        if (x >= size.width)
        {
            throw new IllegalArgumentException("x: " + x + " >= " + size.width);
        }
        if (y >= size.height)
        {
            throw new IllegalArgumentException("y: " + y + " >= " + size.height);
        }
    }
    
    public void set(int x, int y, char c)
    {
        checkXY(x, y);
        room[y][x].setChar(c);
    }
    
    public ColoredChar get(int x, int y)
    {
        checkXY(x, y);
        return room[y][x];
    }
    
    public ColoredChar getChar(int x, int y)
    {
        ColoredChar c = get(x, y).clone();
        // is player?
        if (isPlayerPosition(x, y))
        {
            c = getPlayer().getChar();
        }
        else if (isExplored(x, y))
        {
            if (isMonsterPosition(x, y))
            {
                c = getMonster(x, y).getChar();
            }
            else if (isDoor(x, y))
            {
                c = getDoor(x, y).getChar();
            }
            else
            {
                switch (c.getChar())
                {
                    case Room.WALL:
                    case Room.SUPERWALL:
                    {
                        c.setChar(Walls.getWall(x, y, this));
                        break;
                    }
                    case Room.WATER:
                    {
                        c.setChar('~');
                        c.setConsoleColor(new ConsoleColor(Colors.blue, Colors.black));
                        break;
                    }
                    case Room.LAVA:
                    {
                        c.setChar('~');
                        c.setConsoleColor(new ConsoleColor(Colors.dark_orange, Colors.black));
                    }
                    case Room.CORRIDOR:
                    {
                        c.setChar(BlockElements.BLOCK_50);
                        break;
                    }
                    case Room.FLOOR:
                    {
                        c.setChar(Room.FLOOR);
                        break;
                    }
                    default:
                    {
                        c.setChar(Room.EMPTY);
                        break;
                    }
                }
            }
            if (seeEverything == false && visible.contains(new Point(x, y)) == false)
            {
                c.setConsoleColor(new ConsoleColor(Colors.gray, Colors.black));
            }
        }
        else
        {
            checkExplored(x, y);
            c.setChar(Room.EMPTY);
        }
        return c;
    }
    
    public Player getPlayer()
    {
        return player;
    }
    
    public void setPlayer(Player player)
    {
        this.player = player;
    }
    
    public Point getPlayerPosition()
    {
        return playerPosition;
    }
    
    public void setPlayerPosition(Point playerPosition)
    {
        this.playerPosition = playerPosition;
        if (this.playerPosition != null)
        {
            // set view radius
            char c = get(this.playerPosition.x, this.playerPosition.y).getChar();
            if (c == Room.CORRIDOR)
            {
                setViewRadius(2);
            }
            else
            {
                setViewRadius(5);
            }
        }
    }
    
    public Point getPlayerLookingPosition()
    {
        Point playerPosition = getPlayerPosition();
        int x = playerPosition.x;
        int y = playerPosition.y;
        switch (player.getLookingDirection())
        {
            case NORTH:
            {
                y--;
                break;
            }
            case EAST:
            {
                x++;
                break;
            }
            case SOUTH:
            {
                y++;
                break;
            }
            case WEST:
            {
                x--;
                break;
            }
        }
        return new Point(x, y);
    }
    
    public void doFieldOfView()
    {
        if (this.player == null)
        {
            return;
        }
        int from = 0;
        int to = 360;
        switch (player.getLookingDirection())
        {
            case NORTH:
            {
                from = 225;
                to = 315;
                break;
            }
            case EAST:
            {
                from = 315;
                to = 360;
                break;
            }
            case SOUTH:
            {
                from = 45;
                to = 135;
                break;
            }
            case WEST:
            {
                from = 135;
                to = 225;
                break;
            }
        }
        // clear visible
        if (clearExplored)
        {
            synchronized (explored)
            {
                List<Point> remove = new ArrayList<Point>();
                for (Entry<Point, Boolean> e : explored.entrySet())
                {
                    try
                    {
                        boolean found = false;
                        for (char nc : neverClear)
                        {
                            if (get(e.getKey().x, e.getKey().y).getChar() == nc)
                            {
                                found = true;
                                break;
                            }
                        }
                        if (found == false)
                        {
                            e.setValue(false);
                        }
                    }
                    catch (Exception ex)
                    {
                        remove.add(e.getKey());
                    }
                }
                for (Point point : remove)
                {
                    explored.remove(point);
                }
            }
        }
        visible.clear();
        doFov(from, to, viewRadius);
        if (player.getLookingDirection() == Direction.EAST)
        {
            doFov(0, 45, viewRadius);
        }
    }
    
    private void doFov(int from, int to, int step)
    {
        // fov
        double x;
        double y;
        int i;
        for (i = from; i < to; i+=step)
        {
            x = Math.cos(i * 0.01745f);
            y = Math.sin(i * 0.01745f);
            doFov(x, y);
        }

    }
    
    private void doFov(double x, double y)
    {
        double ox = playerPosition.x + 0.5f;
        double oy = playerPosition.y + 0.5f;
        for (int i = 0; i < getViewRadius(); i++)
        {
            try
            {
                char c = get((int)ox, (int)oy).getChar();
                // Set the tile to visible.
                setExplored((int)ox, (int)oy);
                if (seeEverything == false)
                {
                    addVisible(new Point((int)ox, (int)oy));
                }
                if (c == EMPTY ||
                    c == WALL || 
                    c == SUPERWALL ||
                   (c == DOOR && getDoor((int)ox, (int)oy).isOpen() == false))
                {
                    return;
                }
            }
            catch (IllegalArgumentException ex)
            {
                return;
            }
            ox += x;
            oy += y;
        }
    }
    
    public boolean isPlayerPosition(int x, int y)
    {
        return playerPosition.x == x && playerPosition.y == y;
    }
    
    public boolean isExplored(int x, int y)
    {
        Point xy = new Point(x, y);
        if (explored.containsKey(xy) == false)
        {
            explored.put(xy, false);
        }
        return explored.get(xy);
    }
    
    private void checkExplored(int x, int y)
    {
        // if unexplored but on border and neighbors are explored then explore ;)
        if (isExplored(x, y) == false)
        {
            if (y == 0 && x > 0 && x < getWidth())
            {
                if (isExplored(x - 1, y    ) &&
                    isExplored(x + 1, y    ) &&
                    isExplored(x    , y + 1))
                {
                    setExplored(x, y);
                }
            }
            else if (y == getHeight() - 1 && x > 0 && x < getWidth())
            {
                if (isExplored(x - 1, y    ) &&
                    isExplored(x + 1, y    ) &&
                    isExplored(x    , y - 1))
                {
                    setExplored(x, y);
                }
            }
            else
            {
                if (x == 0 && y > 0 && y < getHeight())
                {
                    if (isExplored(x - 1, y - 1) &&
                        isExplored(x + 1, y    ) &&
                        isExplored(x    , y + 1))
                    {
                        setExplored(x, y);
                    }
                }
                else if (x == getWidth() - 1 && y > 0 && y < getHeight())
                {
                    if (isExplored(x    , y - 1) &&
                        isExplored(x - 1, y    ) &&
                        isExplored(x    , y + 1))
                    {
                        setExplored(x, y);
                    }
                }
            }
        }
    }
    
    public List<Point> getExplored()
    {
        List<Point> result = new ArrayList<Point>();
        for (Point p : explored.keySet())
        {
            if (explored.get(p).booleanValue())
            {
                result.add(p);
            }
        }
        return result;
    }
    
    public void setExplored(int x, int y)
    {
        setExplored(new Point(x, y));
    }
    
    public void setExplored(Point p)
    {
        explored.put(p, true);
    }

    public void exploreAll()
    {
        clearExplored = false;
        for (int y = 0; y < getHeight(); y++)
        {
            for (int x = 0; x < getWidth(); x++)
            {
                setExplored(x, y);
            }
        }
    }
    
    public boolean isSeeEverything()
    {
        return seeEverything;
    }
    
    public void setSeeEverything(boolean seeEverything)
    {
        this.seeEverything = seeEverything;
    }
    
    public void addVisible(Point p)
    {
        visible.add(p);
    }
    
    public List<Point> getVisible()
    {
        return visible;
    }
    
    public boolean isVisible(Point p)
    {
        return visible.contains(p);
    }
    
    public boolean isClearExplored()
    {
        return clearExplored;
    }
    
    public void setClearExplored(boolean clearExplored)
    {
        this.clearExplored = clearExplored;
    }
    
    public char[] getNeverClear()
    {
        return neverClear;
    }
    
    public void setNeverClear(char[] neverClear)
    {
        this.neverClear = neverClear;
    }
    
    public void setViewRadius(int viewRadius)
    {
        this.viewRadius = viewRadius;
    }
    
    public int getViewRadius()
    {
        return viewRadius;
    }
    
    public Map<Point, Door> getDoors()
    {
        return doors;
    }
    
    public boolean isDoor(int x, int y)
    {
        return doors.containsKey(new Point(x, y));
    }
    
    public Door getDoor(int x, int y)
    {
        return doors.get(new Point(x, y));
    }
    
    public void addDoor(int x, int y, Door d)
    {
        doors.put(new Point(x, y), d);
    }
    
    public Map<Point, Monster> getMonsters()
    {
        return monsters;
    }
    
    public boolean isMonsterPosition(int x, int y)
    {
        return monsters.containsKey(new Point(x, y));
    }
    
    public Monster getMonster(Point p)
    {
        Monster m = null;
        if (monsters.containsKey(p))
        {
            m = monsters.get(p);
        }
        return m;
    }
    
    public Monster getMonster(int x, int y)
    {
        return getMonster(new Point(x, y));
    }
    
    public void addMonster(int x, int y, Monster m)
    {
        monsters.put(new Point(x, y), m);
    }
    
    public Point getMonsterPosition(Monster m)
    {
        for (Entry<Point, Monster> e : monsters.entrySet())
        {
            if (e.getValue().equals(m))
            {
                return e.getKey();
            }
        }
        throw new IllegalArgumentException("No such monster: " + m);
    }
    
    public boolean isAccessibleForPlayer(int x, int y)
    {
        boolean accessible = true;
        // or is open door?
        if (isDoor(x, y))
        {
            accessible = getDoor(x, y).isOpen();
        }
        // or is a monster corpse?
        else if (isMonsterPosition(x, y))
        {
            accessible = getMonster(x, y).isAlive() == false;
        }
        else
        {
            char c = get(x, y).getChar();
            // is floor or corridor?
            if (c != Room.FLOOR && c != Room.CORRIDOR)
            {
                accessible = false;
            }
        }
        return accessible;
    }

    public boolean changeRoom()
    {
        return false;
    }

    public Room getRoom(int x, int y)
    {
        return null;
    }
    
    public static Room loadMap(String map)
    {
        String[] lines = map.split("\n");
        int width = -1;
        int y = 0;
        for (String line : lines)
        {
            if (line.endsWith("\r"))
            {
                line = line.substring(0, line.length() - 1);
            }
            if (width == -1)
            {
                width = line.length();
            }
            else
            {
                if (line.length() > width)
                {
                    System.err.println("wrong line width in line " + y + ": " + line.length() + " != " + width);
                    line = line.substring(0, width);
                }
            }
            y++;
        }
        Room room = new Room(width, lines.length);
        int x = 0;
        y = 0;
        for (String line : lines)
        {
            line = line.substring(0, width);
            while (x < line.length())
            {
                char c = line.charAt(x);
                if (c == DOOR)
                {
                    room.getDoors().put(new Point(x, y), new Door());
                }
                else if (c == PLAYER)
                {
                    room.setPlayerPosition(new Point(x, y));
                    c = FLOOR;
                }
                else if (c == MONSTER)
                {
                    Monster m = MonsterFactory.createMonster();
                    room.addMonster(x, y, m);
                    c = FLOOR;
                }
                room.set(x, y, c);
                x++;
            }
            x = 0;
            y++;
        }
        return room;
    }
}
